package br.com.sebrae.pr.sgp.dao.base;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLSyntaxErrorException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.Resource;
import javax.sql.DataSource;

import org.apache.log4j.Logger;

import br.com.sebrae.pr.sgp.entities.base.BasePersistente;
import br.com.sebrae.pr.sgp.util.exceptions.ExcecaoDeBancoDeDados;
import br.com.sebrae.pr.sgp.util.exceptions.ExcecaoDeInfraEstrutura;
import br.com.sebrae.pr.sgp.util.persistence.IConverter;
import br.com.sebrae.pr.sgp.util.persistence.ItemResultset;
import br.com.sebrae.pr.sgp.util.persistence.params.InParam;
import br.com.sebrae.pr.sgp.util.persistence.params.Param;
import br.com.sebrae.pr.sgp.util.persistence.params.ParamType;

import com.google.common.base.Preconditions;

/**
 * Classe base para acesso ao banco de dados.
 * 
 * @author Tanaka
 * 
 */
public abstract class BaseDao {
	private static final Logger log = Logger.getLogger(BaseDao.class);

	@Resource(mappedName = "java:jboss/datasources/PostgreSQL")
	private static DataSource dataSource;

	/**
	 * Obtém a conexão do datasource gerenciado pelo JBOSS e mapeado com
	 * annotation EJB. <br>
	 * <b>Obs: Não alterar o autocommit da connection, pois as transações são
	 * gerenciadas pelo container.</b>
	 * 
	 * @return Conexão
	 */
	public Connection getConnection() {
		Preconditions.checkNotNull(dataSource,
				"Datasource não pode ser obtido!");

		log.debug("Obtendo connection...");

		try {
			final Connection conn = dataSource.getConnection();
			Preconditions.checkNotNull(conn, "Connection não pode ser obtida!");
			log.debug("Connection obtida...");
			return conn;

		} catch (SQLException e) {
			log.fatal("Erro obtendo connection!", e);
			throw new ExcecaoDeInfraEstrutura(e);
		}
	}

	/**
	 * Monta o SQL substituindo os wildcards '?' por seu valor respectivo da
	 * lista de parâmetros.
	 * 
	 * @param call
	 *            Query contendo os '?'
	 * @param params
	 *            parâmetros a serem adicionados à String de retorno
	 * @return query montada com os parâmetros setados.
	 */
	private String montaSqlParaLog(final String call, final List<InParam> params) {
		try {
			final StringBuilder ret = new StringBuilder(call);
			if (params != null && params.size() > 0) {
				for (Param p : params) {
					final InParam ip = (InParam) p;
					ret.replace(ret.indexOf("?"), ret.indexOf("?") + 1, ip
							.getObject() != null ? ip.getObject().toString()
							: "null");
				}
			}
			return ret.toString();
		} catch (Exception e) {
			log.warn("Erro montando SQL para o LOG", e);
		}
		return call + "\n [Parametros:\n" + parametersToString(params) + "]";
	}

	/**
	 * Converte a lista de parâmetros para String, utilizado em caso de erro na
	 * substituição dos parâmetros na montagem do sql para o log.
	 * 
	 * @see BaseDao#montaSqlParaLog(String, List)
	 * 
	 * @param params
	 *            lista de parâmetros
	 * @return String com quebra de linha para cada parâmetro
	 */
	private String parametersToString(final List<InParam> params) {
		final StringBuilder ret = new StringBuilder();
		if (params != null && params.size() > 0) {
			for (Param p : params) {
				if (ret.length() > 0) {
					ret.append("\n");
				}
				final InParam ip = (InParam) p;
				ret.append(p.getParameterIndex() + " [_IN] " + ip.getObject());
			}
		} else {
			ret.append("Nenhum parâmetro...");
		}
		return ret.toString();
	}

	/**
	 * Cria um {@link PreparedStatement} utilizando a connection e o sql
	 * fornecidos. <br>
	 * Valida que nenhum parâmetro seja nulo
	 * 
	 * @see BaseDao#getConnection()
	 * @see BaseDao#prepareStatement(Connection, String)
	 * @param conn
	 *            conexão com o banco de dados
	 * @param sql
	 *            query do banco em um {@link StringBuffer}, será convertida
	 *            para String
	 * @return {@link PreparedStatement}
	 */
	public PreparedStatement prepareStatement(final Connection conn,
			final StringBuffer sql) {
		Preconditions.checkNotNull(conn, "Connection não pode ser nula!");
		Preconditions.checkNotNull(sql, "SQL não pode ser nulo!");
		return prepareStatement(conn, sql.toString());
	}

	/**
	 * Cria um {@link PreparedStatement} utilizando a connection e o sql
	 * fornecidos. <br>
	 * Valida que nenhum parâmetro seja nulo, assim como após criar o
	 * {@link PreparedStatement}, valida se o mesmo não é nulo!
	 * 
	 * @see BaseDao#getConnection()
	 * @param conn
	 *            conexão com o banco de dados
	 * @param sql
	 *            query do banco como String
	 * @return {@link PreparedStatement}
	 */
	public PreparedStatement prepareStatement(final Connection conn,
			final String sql) {
		Preconditions.checkNotNull(conn, "Connection não pode ser nula!");
		Preconditions.checkNotNull(sql, "SQL não pode ser nulo!");
		PreparedStatement ps = null;
		try {
			ps = conn.prepareStatement(sql);
			Preconditions.checkNotNull(ps,
					"PreparedStatement não pode ser obtido!");

		} catch (SQLException e) {
			log.error("Erro obtendo PreparedStatement! SQL: '"
					+ montaSqlParaLog(sql, null) + "'.", e);
			throw new ExcecaoDeInfraEstrutura(e);

		}
		return ps;
	}

	/**
	 * Fecha a {@link Connection} fornecida, valida se a mesma não é nula e se a
	 * mesma já não se encontra fechada. <br>
	 * Este método não lança nenhuma exceção.
	 * 
	 * @param con
	 */
	public void close(final Connection con) {
		if (con == null) {
			return;
		}
		try {
			if (con.isClosed()) {
				return;
			}
		} catch (Exception e) {
		}
		try {
			con.close();
		} catch (Exception e) {
			log.error("Erro fechando Connection!", e);
		}
	}

	/**
	 * Fecha o {@link PreparedStatement} fornecido, valida se o mesmo não é nulo
	 * e se o mesmo já não se encontra fechado. <br>
	 * Este método não lança nenhuma exceção.
	 * 
	 * @param ps
	 */
	public void close(final PreparedStatement ps) {
		if (ps == null) {
			return;
		}
		try {
			if (ps.isClosed()) {
				return;
			}
		} catch (Exception e) {
		}
		try {
			ps.close();
		} catch (Exception e) {
			log.error("Erro fechando PreparedStatement!", e);
		}
	}

	/**
	 * Fecha o {@link ResultSet} fornecido, valida se o mesmo não é nulo e se o
	 * mesmo já não se encontra fechado. <br>
	 * Este método não lança nenhuma exceção.
	 * 
	 * @param rs
	 */
	public void close(final ResultSet rs) {
		if (rs == null) {
			return;
		}
		try {
			if (rs.isClosed()) {
				return;
			}
		} catch (Exception e) {
		}
		try {
			rs.close();
		} catch (Exception e) {
			log.error("Erro fechando ResultSet!", e);
		}
	}

	/**
	 * Executa a query utilizando os parâmetros fornecidos (pode ser null).
	 * Quando obtido o {@link ResultSet} utiliza o {@link IConverter} para
	 * converter a linha do {@link ResultSet} em um objeto do tipo desejado.
	 * 
	 * @see IConverter
	 * @see BaseDao#executarQuery(String, List, PreparedStatement, IConverter)
	 * 
	 * @param sql
	 *            query padrão oracle, com os parâmetros na forma ?, será
	 *            convertida para string
	 * @param params
	 *            parâmetros para substituição
	 * @param converter
	 *            conversor para o tipo de retorno desejado
	 * @return lista tipada do tipo desejado. O tipo do retorno será definido
	 *         pelo tipo do {@link IConverter}
	 */
	public <T2> List<T2> executarQuery(final StringBuffer sql,
			final List<InParam> params, final IConverter<T2> converter) {
		return executarQuery(sql.toString(), params, converter);
	}

	/**
	 * Executa a query utilizando os parâmetros fornecidos (pode ser null).
	 * Quando obtido o {@link ResultSet} utiliza o {@link IConverter} para
	 * converter a linha do {@link ResultSet} em um objeto do tipo desejado.
	 * 
	 * @see IConverter
	 * 
	 * @param sql
	 *            query padrão oracle, com os parâmetros na forma ?
	 * @param params
	 *            parâmetros para substituição
	 * @param converter
	 *            conversor para o tipo de retorno desejado
	 * @return lista tipada do tipo desejado. O tipo do retorno será definido
	 *         pelo tipo do {@link IConverter}
	 */
	public <T2> List<T2> executarQuery(final String sql,
			final List<InParam> params, final IConverter<T2> converter) {
		PreparedStatement ps = null;
		Connection conn = null;
		try {
			conn = getConnection();
			ps = prepareStatement(conn, sql);
			if (params != null && !params.isEmpty()) {
				setParams(ps, params);
			}
			return executarQuery(sql, params, ps, converter);

		} finally {
			close(ps);
			close(conn);
		}
	}

	/**
	 * Executa o update utilizando os parâmetros fornecidos (pode ser null).
	 * 
	 * @see BaseDao#executarUpdate(String, List)
	 * 
	 * @param sql
	 *            padrão oracle, com os parâmetros na forma ?, será convertido
	 *            para String
	 * @param params
	 *            parâmetros para substituição
	 * @return quantidade de registros alterados
	 */
	public int executarUpdate(final StringBuffer sql, final List<InParam> params) {
		return executarUpdate(sql.toString(), params);
	}

	/**
	 * Executa o update utilizando os parâmetros fornecidos (pode ser null).
	 * 
	 * @param sql
	 *            padrão oracle, com os parâmetros na forma ?, será convertido
	 *            para String
	 * @param params
	 *            parâmetros para substituição
	 * @return quantidade de registros alterados
	 */
	public int executarUpdate(final String sql, final List<InParam> params) {
		PreparedStatement ps = null;
		Connection conn = null;
		final long timeInicio = System.currentTimeMillis();
		try {
			conn = getConnection();
			ps = prepareStatement(conn, sql);
			if (params != null && !params.isEmpty()) {
				setParams(ps, params);
			}

			log.debug("Executando update. ID: " + timeInicio + ".");
			return ps.executeUpdate();

		} catch (SQLException e) {
			if (e instanceof SQLSyntaxErrorException && sql.indexOf(";") >= 0) {
				log.error(
						"Erro executando update, ID: "
								+ timeInicio
								+ ". SQL: ['"
								+ montaSqlParaLog(sql, params)
								+ "'], verifique se o comando não acaba com ';', isto pode ocasionar erros no oracle.",
						e);

			} else {
				log.error("Erro executando update, ID: " + timeInicio
						+ ". SQL: ['" + montaSqlParaLog(sql, params) + "']", e);

			}
			throw new ExcecaoDeInfraEstrutura(e);

		} finally {
			final long millisExecucao = System.currentTimeMillis() - timeInicio;
			if (millisExecucao >= 900) {
				log.warn("Término execução query. ID: " + timeInicio
						+ ", SQL: ['" + montaSqlParaLog(sql, params)
						+ "'], demorou: " + millisExecucao + " millis.");

			} else {
				log.debug("Término execução query. ID: " + timeInicio
						+ ", SQL: ['" + montaSqlParaLog(sql, params)
						+ "'], demorou: " + millisExecucao + " millis.");
			}
			close(ps);
			close(conn);
		}
	}

	/**
	 * Adiciona os parâmetros recebidos ao {@link PreparedStatement}.
	 * 
	 * @see InParam
	 * 
	 * @param ps
	 *            {@link PreparedStatement} não pode ser nulo
	 * @param params
	 *            lista de parâmetros, podendo ser null
	 */
	public static void setParams(final PreparedStatement ps,
			final List<InParam> params) {
		Preconditions.checkNotNull(ps);
		int i = 0;
		try {
			if (params == null) {
				return;
			}
			for (i = 0; i < params.size(); i++) {
				final InParam ip = params.get(i);
				if (ip.getObject() != null) {
					if (ip.getObject() instanceof java.util.Date) {
						// Exceções à regra, se o type informado for date e o
						// tipo do objeto for java.util.date, converte para
						// java.sql.Timestamp, que não perde a hora, pode ser um
						// campo DATE no oracle...
						final java.util.Date dta = (java.util.Date) ip
								.getObject();
						ps.setTimestamp(ip.getParameterIndex(),
								new java.sql.Timestamp(dta.getTime()));

					} else {
						ps.setObject(ip.getParameterIndex(), ip.getObject(), ip
								.getType().getType());
					}

				} else {
					ps.setNull(ip.getParameterIndex(), ip.getType().getType());

				}
			}
		} catch (SQLException e) {
			log.error("Erro setando parametros no CallableStatement! Param: "
					+ params.get(i), e);
			throw new ExcecaoDeInfraEstrutura(e);
		}
	}

	/**
	 * Executa a query do {@link PreparedStatement} informado, quando obtido o
	 * {@link ResultSet} utiliza o {@link IConverter} para converter a linha do
	 * {@link ResultSet} em um objeto do tipo desejado.
	 * 
	 * @see IConverter
	 * 
	 * @param sql
	 *            query padrão oracle, com os parâmetros na forma ?,
	 *            <b>utilizada somente para log</b>
	 * @param params
	 *            parâmetros para substituição, <b>utilizado somente para
	 *            log</b>
	 * @param ps
	 *            {@link PreparedStatement} a ser executado
	 * @param converter
	 *            conversor para o tipo de retorno desejado
	 * @return lista tipada do tipo desejado. O tipo do retorno será definido
	 *         pelo tipo do {@link IConverter}
	 */
	private <T2> List<T2> executarQuery(final String sql,
			final List<InParam> params, final PreparedStatement ps,
			final IConverter<T2> converter) {
		final List<T2> ret = new ArrayList<T2>();
		ResultSet rs = null;
		final long timeInicio = System.currentTimeMillis();
		try {
			log.debug("Executando query. ID: " + timeInicio + ".");
			rs = ps.executeQuery();
			while (rs.next()) {
				final int qtdColunas = rs.getMetaData().getColumnCount();
				final Map<String, Object> colunas = new HashMap<String, Object>();
				for (int i = 0; i < qtdColunas; i++) {
					final Object value = rs.getObject(i + 1);

					if (rs.getMetaData().getColumnType(i + 1) == ParamType.CLOB
							.getType()) {
						colunas.put(rs.getMetaData().getColumnName(i + 1)
								.toUpperCase(), rs.getString(i + 1));

					} else {
						colunas.put(rs.getMetaData().getColumnName(i + 1)
								.toUpperCase(), value);
					}
				}
				ret.add(converter.convert(new ItemResultset(colunas)));
			}

		} catch (SQLException e) {
			log.error("Erro executando update, ID: " + timeInicio + ". SQL: ['"
					+ montaSqlParaLog(sql, params) + "']", e);
			throw new ExcecaoDeBancoDeDados(e);

		} catch (Exception e) {
			log.error("Erro executando update, ID: " + timeInicio + ". SQL: ['"
					+ montaSqlParaLog(sql, params) + "']", e);
			throw new ExcecaoDeInfraEstrutura(e);

		} finally {
			final long millisExecucao = System.currentTimeMillis() - timeInicio;
			if (millisExecucao >= 900) {
				log.warn("Término execução query. ID: " + timeInicio
						+ ", SQL: ['" + montaSqlParaLog(sql, params)
						+ "'], demorou: " + millisExecucao + " millis.");

			} else {
				log.debug("Término execução query. ID: " + timeInicio
						+ ", SQL: ['" + montaSqlParaLog(sql, params)
						+ "'], demorou: " + millisExecucao + " millis.");
			}
			close(rs);
			close(ps);
		}
		return ret;
	}

	/**
	 * Verifica se o id de dado dto é nulo ou zero, indicando que ainda não foi
	 * persistido.
	 * 
	 * @param dto
	 *            não pode ser nulo
	 * @return true se o id for nulo ou zero
	 */
	public boolean inclusao(final BasePersistente dto) {
		Preconditions.checkNotNull(dto, "BasePersistente é obrigatório");
		return dto.getId() == null || dto.getId() == 0;
	}

	/**
	 * Verifica se o id de dado dto é diferente de nulo e maior que zero,
	 * indicando que já se encontra persistido.
	 * 
	 * @param dto
	 *            não pode ser nulo
	 * @return true se o id for diferente de nulo e maior que zero
	 */
	public boolean alteracao(final BasePersistente dto) {
		Preconditions.checkNotNull(dto, "BasePersistente é obrigatório");
		return dto.getId() != null && dto.getId() > 0;
	}

}
